import org.apache.commons.exec.CommandLine
import org.apache.commons.exec.DefaultExecutor
import org.apache.commons.exec.ExecuteException
import org.apache.commons.exec.PumpStreamHandler
import org.slf4j.LoggerFactory
import java.io.*
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.file.attribute.PosixFilePermission
import java.security.MessageDigest
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.math.pow
import java.text.StringCharacterIterator
import java.text.CharacterIterator
import kotlin.system.exitProcess

System.setProperty("org.slf4j.simpleLogger.showDateTime", "true")
System.setProperty("org.slf4j.simpleLogger.dateTimeFormat", "MM.dd_HH:mm:ss")
System.setProperty("org.slf4j.simpleLogger.showThreadName", "false")
System.setProperty("org.slf4j.simpleLogger.showLogName", "false")

val log = LoggerFactory.getLogger("TC#0")

data class Slice(
    var srcFile: String,
    var offset: Int,
    var length: Int,
    var dumpFile: String
)

fun joinWithNulls(vararg source: ByteArray?): ByteArray {
    val baos = ByteArrayOutputStream()
    for (src in source) {
        src?.let {
            if (src.isNotEmpty()) baos.write(src)
        }
    }
    return baos.toByteArray()
}

@OptIn(kotlin.ExperimentalUnsignedTypes::class)
fun toHexString(inData: UByteArray): String {
    val sb = StringBuilder()
    for (i in inData.indices) {
        sb.append(Integer.toString((inData[i].toInt().and(0xff)) + 0x100, 16).substring(1))
    }
    return sb.toString()
}

fun toHexString(inData: ByteArray): String {
    val sb = StringBuilder()
    for (i in inData.indices) {
        sb.append(Integer.toString((inData[i].toInt().and(0xff)) + 0x100, 16).substring(1))
    }
    return sb.toString()
}

fun fromHexString(s: String): ByteArray {
    val len = s.length
    val data = ByteArray(len / 2)
    var i = 0
    while (i < len) {
        data[i / 2] = ((Character.digit(s[i], 16) shl 4) + Character.digit(s[i + 1], 16)).toByte()
        i += 2
    }
    return data
}

fun extractFile(s: Slice) {
    return extractFile(s.srcFile, s.dumpFile, s.offset.toLong(), s.length)
}

fun extractFile(fileName: String, outImgName: String, offset: Long, length: Int) {
    if (0 == length) {
        return
    }
    RandomAccessFile(fileName, "r").use { inRaf ->
        RandomAccessFile(outImgName, "rw").use { outRaf ->
            inRaf.seek(offset)
            val data = ByteArray(length)
            assert(length == inRaf.read(data))
            outRaf.write(data)
        }
    }
}

fun round_to_multiple(size: UInt, page: UInt): UInt {
    val remainder = size % page
    return if (remainder == 0U) {
        size
    } else {
        size + page - remainder
    }
}

fun round_to_multiple(size: Long, page: Int): Long {
    val remainder = size % page
    return if (remainder == 0L) {
        size
    } else {
        size + page - remainder
    }
}

fun round_to_multiple(size: Int, page: Int): Int {
    val remainder = size % page
    return if (remainder == 0) {
        size
    } else {
        size + page - remainder
    }
}

fun round_to_pow2(num: Long): Long {
    return 2.0.pow((num - 1).toBigInteger().bitLength().toDouble()).toLong()
}

fun pyAlg2java(alg: String): String {
    return when (alg) {
        "sha1" -> "sha-1"
        "sha224" -> "sha-224"
        "sha256" -> "sha-256"
        "sha384" -> "sha-384"
        "sha512" -> "sha-512"
        else -> throw IllegalArgumentException("unknown algorithm: [$alg]")
    }
}

fun dumpToFile(dumpFile: String, data: ByteArray) {
    log.info("Dumping data to $dumpFile ...")
    FileOutputStream(dumpFile, false).use { fos ->
        fos.write(data)
    }
    log.info("Dumping data to $dumpFile done")
}

fun String.deleteIfExists() {
    if (File(this).exists()) {
        log.info("deleting $this")
        File(this).delete()
    }
}

fun String.check_call(inWorkdir: String? = null): Boolean {
    val ret: Boolean
    try {
        val cmd = CommandLine.parse(this)
        log.run {
            info("CMD: $cmd, workDir: $inWorkdir")
        }
        val exec = DefaultExecutor()
        inWorkdir?.let { exec.workingDirectory = File(it) }
        exec.execute(cmd)
        ret = true
    } catch (e: java.lang.IllegalArgumentException) {
        log.error("$e: can not parse command: [$this]")
        throw e
    } catch (e: ExecuteException) {
        log.error("$e: can not exec command")
        throw e
    } catch (e: IOException) {
        log.error("$e: can not exec command")
        throw e
    }
    return ret
}

fun String.call(inWorkdir: String? = null): Boolean {
    try {
        return this.check_call(inWorkdir)
    } catch (e: Exception) {
        log.info("$this fail, ignore")
        return false
    }
}

fun String.check_output(): String {
    val outputStream = ByteArrayOutputStream()
    log.info(this)
    DefaultExecutor().let {
        it.streamHandler = PumpStreamHandler(outputStream)
        it.execute(CommandLine.parse(this))
    }
    log.info(outputStream.toString().trim())
    return outputStream.toString().trim()
}

fun String.pumpRun(): Array<ByteArrayOutputStream> {
    val outStream = ByteArrayOutputStream()
    val errStream = ByteArrayOutputStream()
    log.info("CMD: $this")
    DefaultExecutor().let {
        it.streamHandler = PumpStreamHandler(outStream, errStream)
        it.execute(CommandLine.parse(this))
    }
    log.info("stdout [$outStream]")
    log.info("stderr [$errStream]")
    return arrayOf(outStream, errStream)
}

fun powerRun3(cmdline: CommandLine, inputStream: InputStream?): Array<Any> {
    var ret = true
    val outStream = ByteArrayOutputStream()
    val errStream = ByteArrayOutputStream()
    log.info("CMD: $cmdline")
    try {
        DefaultExecutor().let {
            it.streamHandler = PumpStreamHandler(outStream, errStream, inputStream)
            it.execute(cmdline)
        }
    } catch (e: ExecuteException) {
        log.error("fail to execute [${cmdline}]")
        ret = false
    }
    log.debug("stdout [$outStream]")
    log.debug("stderr [$errStream]")
    return arrayOf(ret, outStream.toByteArray(), errStream.toByteArray())
}

fun powerRun2(cmd: String, inputStream: InputStream?): Array<Any> {
    var ret = true
    val outStream = ByteArrayOutputStream()
    val errStream = ByteArrayOutputStream()
    log.info("CMD: $cmd")
    try {
        DefaultExecutor().let {
            it.streamHandler = PumpStreamHandler(outStream, errStream, inputStream)
            it.execute(CommandLine.parse(cmd))
        }
    } catch (e: ExecuteException) {
        log.error("fail to execute [$cmd]")
        ret = false
    }
    log.debug("stdout [$outStream]")
    log.debug("stderr [$errStream]")
    return arrayOf(ret, outStream.toByteArray(), errStream.toByteArray())
}

fun powerRun(cmd: String, inputStream: InputStream?, bSilent: Boolean = false): Array<ByteArray> {
    val outStream = ByteArrayOutputStream()
    val errStream = ByteArrayOutputStream()
    if (!bSilent) log.info("CMD: $cmd")
    try {
        DefaultExecutor().let {
            it.streamHandler = PumpStreamHandler(outStream, errStream, inputStream)
            it.execute(CommandLine.parse(cmd))
        }
    } catch (e: ExecuteException) {
        log.error("fail to execute [$cmd]")
    }
    log.debug("stdout [$outStream]")
    log.debug("stderr [$errStream]")
    return arrayOf(outStream.toByteArray(), errStream.toByteArray())
}

fun hashFileAndSize(vararg inFiles: String?): ByteArray {
    val md = MessageDigest.getInstance("SHA1")
    for (item in inFiles) {
        if (null == item) {
            md.update(
                ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN)
                    .putInt(0)
                    .array()
            )
            log.debug("update null $item: " + toHexString((md.clone() as MessageDigest).digest()))
        } else {
            val currentFile = File(item)
            FileInputStream(currentFile).use { iS ->
                var byteRead: Int
                val dataRead = ByteArray(1024)
                while (true) {
                    byteRead = iS.read(dataRead)
                    if (-1 == byteRead) {
                        break
                    }
                    md.update(dataRead, 0, byteRead)
                }
                log.debug("update file $item: " + toHexString((md.clone() as MessageDigest).digest()))
                md.update(
                    ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN)
                        .putInt(currentFile.length().toInt())
                        .array()
                )
                log.debug("update SIZE $item: " + toHexString((md.clone() as MessageDigest).digest()))
            }
        }
    }

    return md.digest()
}

fun assertFileEquals(file1: String, file2: String) {
    val hash1 = hashFileAndSize(file1)
    val hash2 = hashFileAndSize(file2)
    log.info("$file1 hash ${toHexString(hash1)}, $file2 hash ${toHexString(hash2)}")
    if (hash1.contentEquals(hash2)) {
        log.info("Hash verification passed: ${toHexString(hash1)}")
    } else {
        log.error("Hash verification failed")
        throw UnknownError("Do not know why hash verification fails, maybe a bug")
    }
}

fun modeToPermissions(inMode: Int): Set<PosixFilePermission> {
    var mode = inMode
    mode = mode and Integer.valueOf("7777", 8) //trim to xxxx
    val maxSupportedMode = Integer.valueOf("777", 8) //setgid/setuid/sticky are not supported
    if (mode and maxSupportedMode != mode) {
        throw IOException("Invalid mode(oct): ${Integer.toOctalString(mode)}")
    }
    val allPermissions = PosixFilePermission.values()
    val result: MutableSet<PosixFilePermission> = EnumSet.noneOf(PosixFilePermission::class.java)
    for (i in allPermissions.indices) {
        if (mode and 1 == 1) {
            result.add(allPermissions[allPermissions.size - i - 1])
        }
        mode = mode shr 1
    }
    return result
}

/*
  https://stackoverflow.com/questions/3758606/how-can-i-convert-byte-size-into-a-human-readable-format-in-java
 */
fun humanReadableByteCountBin(bytes: Long): String {
    val absB = if (bytes == Long.MIN_VALUE) Long.MAX_VALUE else Math.abs(bytes)
    if (absB < 1024) {
        return "$bytes B"
    }
    var value = absB
    val ci: CharacterIterator = StringCharacterIterator("KMGTPE")
    var i = 40
    while (i >= 0 && absB > 0xfffccccccccccccL shr i) {
        value = value shr 10
        ci.next()
        i -= 10
    }
    value *= java.lang.Long.signum(bytes).toLong()
    return String.format("%.1f %ciB", value / 1024.0, ci.current())
}

fun waitForBootComplete() {
    while (true) {
        val bootComp = String(powerRun("adb shell getprop sys.boot_completed", null)[0]).trim()
        if (bootComp == "1") {
            log.info("Android boot complete")
            break
        }
        TimeUnit.SECONDS.sleep(10)
    }
}

fun waitForRecoveryModeAdb() {
    while (true) {
        val stdoutStr = String(powerRun("adb devices", null, true)[0]).trim()
        if (stdoutStr.contains("recovery")) {
            log.info("adb in recovery mode")
            stdoutStr.split("\n".toRegex()).forEach { log.info(it) }
            break
        }
        TimeUnit.SECONDS.sleep(2)
    }
}

fun waitForNormalModeAdb() {
    while (true) {
        val stdoutStr = String(powerRun("adb devices", null, true)[0]).trim()
        if (stdoutStr.endsWith("device")) {
            log.info("adb in normal mode")
            stdoutStr.split("\n".toRegex()).forEach { log.info(it) }
            break
        }
        TimeUnit.SECONDS.sleep(5)
    }
}

fun doSth(inRoundNo: Int) {
    "adb wait-for-device".check_call()
    "adb root".call()
    "adb wait-for-device".check_call()
    waitForBootComplete()
    "adb shell test_disp hdcp setVersion 0".check_call()
    "adb shell log -t cfig Test_Round_${inRoundNo}_phase_1".check_call()
    "adb shell su 0 am broadcast -a android.intent.action.FACTORY_RESET -p android".check_call()
    "adb shell su 0 am broadcast -a android.intent.action.FACTORY_RESET -p android".call()

    log.info("Waiting for recovery ...")
    TimeUnit.SECONDS.sleep(20)
    //waitForRecoveryModeAdb()

    log.info("Waiting for normal ...")
    TimeUnit.SECONDS.sleep(60)
    waitForNormalModeAdb()

    "adb wait-for-device".check_call()
    "adb root".call()
    "adb wait-for-device".check_call()
    "adb root".check_call()
    waitForBootComplete()
    "adb shell test_disp hdcp setVersion 0".check_call()
    "adb shell log -t cfig Test_Round_${inRoundNo}_phase_2".check_call()
    "adb shell cat /proc/interrupts".check_call()
    val outStr = "adb shell ifconfig eth0".check_output()
    val searchRet = outStr.split("\n".toRegex()).filter { "inet[6]? addr".toRegex().find(it) != null }
    searchRet.forEach {
        log.info("Found IP: " + it.trim())
    }
    if (searchRet.size > 0) {
        log.info("TEST PASS")
    } else {
        log.info("TEST FAILURE")
        "adb shell cat /proc/interrupts".check_call()
        exitProcess(1)
    }
    println("")
}

for (i in 1..30) {
    log.info("Round #$i")
    doSth(i)
}

